/***** BEGIN LICENSE BLOCK *****
 * Version: CPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Common Public
 * License Version 1.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.eclipse.org/legal/cpl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2001 Chad Fowler <chadfowler@chadfowler.com>
 * Copyright (C) 2001 Alan Moore <alan_moore@gmx.net>
 * Copyright (C) 2001 Ed Sinjiashvili <slorcim@users.sourceforge.net>
 * Copyright (C) 2001-2004 Jan Arne Petersen <jpetersen@uni-bonn.de>
 * Copyright (C) 2002 Benoit Cerrina <b.cerrina@wanadoo.fr>
 * Copyright (C) 2002-2006 Thomas E Enebo <enebo@acm.org>
 * Copyright (C) 2002-2004 Anders Bengtsson <ndrsbngtssn@yahoo.se>
 * Copyright (C) 2004 Stefan Matthias Aust <sma@3plus4.de>
 * Copyright (C) 2005 Charles O Nutter <headius@headius.com>
 * Copyright (C) 2006 Miguel Covarrubias <mlcovarrubias@gmail.com>
 * 
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the CPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the CPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/
package asruby.lang {

import asruby.Ruby;
import asruby.runtime.Block;
import asruby.runtime.CallbackFactory;
import asruby.runtime.IRubyObject;
import asruby.runtime.MethodIndex;
import asruby.runtime.ThreadContext;

/**
 * @author jpetersen
 */
public class RubyRange extends RubyObject {

    private  var begin:IRubyObject;
    private  var end:IRubyObject;
    private  var isExclusive:Boolean;

    public  function RubyRange(runtime:Ruby, impl:RubyClass){
        super(runtime, impl);
        begin = end = runtime.getNil();
    }

    public  function init(context:ThreadContext, aBegin:IRubyObject, aEnd:IRubyObject, aIsExclusive:RubyBoolean):void{
        if (!(aBegin instanceof RubyFixnum && aEnd instanceof RubyFixnum)) {
            try {
                var result: IRubyObject =  aBegin.callMethod(context, MethodIndex.OP_SPACESHIP, "<=>", aEnd);
                if (result.isNil()) throw getRuntime().newArgumentError("bad value for range");
            } catch (rExcptn:RaiseException) {
                throw getRuntime().newArgumentError("bad value for range");
            }
        }

        this.begin = aBegin;
        this.end = aEnd;
        this.isExclusive = aIsExclusive.isTrue();
    }
    
    private static var RANGE_ALLOCATOR: Function =
        function allocate(runtime:Ruby, klass:RubyClass):IRubyObject{
            return new RubyRange(runtime, klass);
        };
    
/*     protected override function copySpecialInstanceVariables(clone:IRubyObject):void{
        var range: RubyRange =  clone;
        range.begin = begin;
        range.end = end;
        range.isExclusive = isExclusive;
    } 

    private static var RANGE_MARSHAL: ObjectMarshal =  new ObjectMarshal( 
        function marshalTo(runtime:Ruby, obj:Object, type:RubyClass, marshalStream:MarshalStream):void {
            var range: RubyRange =  obj;

            marshalStream.registerLinkTarget(range);
            List<Variable> attrs = range.getVariableList();

            attrs.add(new VariableEntry("begin", range.begin));
            attrs.add(new VariableEntry("end", range.end));
            attrs.add(new VariableEntry(
                    "excl", range.isExclusive ? runtime.getTrue() : runtime.getFalse()));

            marshalStream.dumpVariables(attrs);
        }
	    ,
        function unmarshalFrom(runtime:Ruby, type:RubyClass, unmarshalStream:UnmarshalStream):Object  {
            var range: RubyRange =  type.allocate();
            
            unmarshalStream.registerLinkTarget(range);

            unmarshalStream.defaultVariablesUnmarshal(range);
            
            range.begin = range.removeInternalVariable("begin");
            range.end = range.removeInternalVariable("end");
            range.isExclusive = range.removeInternalVariable("excl").isTrue();

            return range;
        }
    );
*/    
    public static function createRangeClass(runtime:Ruby):RubyClass{
        var result: RubyClass =  runtime.defineClass("Range", runtime.getObject(), RANGE_ALLOCATOR);
        runtime.setRange(result);
        result.kindOf =
                function isKindOf(obj:IRubyObject, type:RubyModule):Boolean{
                    return obj instanceof RubyRange;
                };
        
        result.setMarshal(RANGE_MARSHAL);
        
        var callbackFactory: CallbackFactory =  runtime.callbackFactory;
        
        result.includeModule(runtime.getEnumerable());

        // We override Enumerable#member? since ranges in 1.8.1 are continuous.
        //        result.defineMethod("member?", callbackFactory.getMethod("include_p", RubyKernel.IRUBY_OBJECT));
        //        result.defineMethod("===", callbackFactory.getMethod("include_p", RubyKernel.IRUBY_OBJECT));

        result.defineAnnotatedMethods;
        
//        CallbackFactory classCB = runtime.callbackFactory(RubyClass);
//        result.getMetaClass().defineMethod("new", classCB.getOptMethod("newInstance"));
        
        result.dispatcher = callbackFactory.createDispatcher(result);
        
        return result;
    }

    /**
     * Converts this Range to a pair of integers representing a start position 
     * and length.  If either of the range's endpoints is negative, it is added to 
     * the limit</code> parameter in an attempt to arrive at a position 
     * p</i> such that 0&nbsp;&lt;=&nbsp;p&nbsp;&lt;=&nbsp;limit</i>. If 
     * truncate</code> is true, the result will be adjusted, if possible, so 
     * that begin&nbsp;+&nbsp;length&nbsp;&lt;=&nbsp;limit</i>.  If strict</code> 
     * is true, an exception will be raised if the range can't be converted as 
     * described above; otherwise it just returns null</b>. 
     * 
     * @param limit    the size of the object (e.g., a String or Array) that 
     *                 this range is being evaluated against.
     * @param truncate if true, result must fit within the range (0..limit)</i>.
     * @param isStrict   if true, raises an exception if the range can't be converted.
     * @return         a two-element array representing a start value and a length, 
     *                 or null</b> if the conversion failed.
     */
    public  function getBeginLength(limit:Number, truncate:Boolean, isStrict:Boolean):Array{
        var beginLong: Number =  RubyNumeric.num2long(begin);
        var endLong: Number =  RubyNumeric.num2long(end);

        // Apparent legend for MRI 'err' param to JRuby 'truncate' and 'isStrict':
        // 0 =>  truncate && !strict
        // 1 => !truncate &&  strict
        // 2 =>  truncate &&  strict
        if (!isExclusive) {
            endLong++;
        }

        if (beginLong < 0) {
            beginLong += limit;
            if (beginLong < 0) {
                if (isStrict) {
                    throw getRuntime().newRangeError(inspect().toString() + " out of range.");
                }
                return null;
            }
        }

        if (truncate && beginLong > limit) {
            if (isStrict) {
                throw getRuntime().newRangeError(inspect().toString() + " out of range.");
            }
            return null;
        }

        if (truncate && endLong > limit) {
            endLong = limit;
        }

        if (endLong < 0 || (!isExclusive && endLong == 0)) {
            endLong += limit;
            // don't check against negative endLong,
            // that's how MRI behaves.
        }

        return [beginLong, Math.max(endLong - beginLong, 0)];
    }

    public  function begLen(len:Number, err:int):Array{
        var beg: Number =  RubyNumeric.num2long(this.begin);
        var end: Number =  RubyNumeric.num2long(this.end);

        if(beg < 0){
            beg += len;
            if(beg < 0){
                if(err != 0){
                    throw getRuntime().newRangeError(beg + ".." + (isExclusive ? "." : "") + end + " out of range");
                }
                return null;
            }
        }

        if(err == 0 || err == 2){
            if(beg > len){
                if(err != 0){
                    throw getRuntime().newRangeError(beg + ".." + (isExclusive ? "." : "") + end + " out of range");
                }
                return null;
            }
            if(end > len){
                end = len;
            }
        }
        if(end < 0){
            end += len;
        }
        if(!isExclusive){
            end++;
        }
        len = end - beg;
        if(len < 0){
            len = 0;
        }

        return [beg, len];
    }    

    public static function newRange(runtime:Ruby, context:ThreadContext, begin:IRubyObject, end:IRubyObject, isExclusive:Boolean):RubyRange{
        var range: RubyRange =  new RubyRange(runtime, runtime.getRange());
        range.init(context, begin, end, isExclusive ? runtime.getTrue() : runtime.getFalse());
        return range;
    }

    public override function initialize(context:ThreadContext, args:Array, unusedBlock:Block):IRubyObject{
        if (args.length == 3) {
            init(context, args[0], args[1], args[2].isTrue() ? getRuntime().getTrue() : getRuntime().getFalse());
        } else if (args.length == 2) {
            init(context, args[0], args[1], getRuntime().getFalse());
        } else {
            throw getRuntime().newArgumentError("Wrong arguments. (anObject, anObject, aBoolean = false) expected");
        }
        return getRuntime().getNil();
    }

    public  function first():IRubyObject{
        return begin;
    }

    public  function last():IRubyObject{
        return end;
    }
    
    public override function hash(context:ThreadContext):RubyFixnum{
        var baseHash: Number =  (isExclusive ? 1 : 0);
        var beginHash: Number =  ( begin.callMethod(context, MethodIndex.HASH, "hash")).getLongValue();
        var endHash: Number =  ( end.callMethod(context, MethodIndex.HASH, "hash")).getLongValue();
        
        var hash: Number =  baseHash;
        hash = hash ^ (beginHash << 1);
        hash = hash ^ (endHash << 9);
        hash = hash ^ (baseHash << 24);
        
        return getRuntime().newFixnum(hash);
    }
    
    private static var DOTDOTDOT: Array =  "...".getBytes();
    private static var DOTDOT: Array =  "..".getBytes();

/*     public override function inspect(context:ThreadContext, block:Block):IRubyObject{
        var str: RubyString =  RubyString.objAsString(context, begin.callMethod(context, "inspect")).strDup();
        var str2: RubyString =  RubyString.objAsString(context, end.callMethod(context, "inspect"));

        str.cat(isExclusive ? DOTDOTDOT : DOTDOT);
        str.concat(str2);
        str.infectBy(str2);
        return str;
    } */
    
    public override function to_s(context:ThreadContext, block:Block):IRubyObject{
        var str: RubyString =  RubyString.objAsString(context, begin).strDup();
        var str2: RubyString =  RubyString.objAsString(context, end);

        str.cat(isExclusive ? DOTDOTDOT : DOTDOT);
        str.concat(str2);
        str.infectBy(str2);
        return str;

    }

    public  function exclude_end_p():RubyBoolean{
        return getRuntime().newBoolean(isExclusive);
    }

    public  function length(context:ThreadContext, block:Block):RubyFixnum{
        var size: Number =  0;

        if (begin.callMethod(context, MethodIndex.OP_GT, ">", end).isTrue()) {
            return getRuntime().newFixnum(0);
        }

        if (begin instanceof RubyFixnum && end instanceof RubyFixnum) {
            size = ( end).getLongValue() - ( begin).getLongValue();
            if (!isExclusive) {
                size++;
            }
        } else { // Support length for arbitrary classes
            var currentObject: IRubyObject =  begin;
	    var compareMethod: int =  isExclusive ? MethodIndex.OP_LT : MethodIndex.OP_LE;

	    while (currentObject.callMethod(context, compareMethod, MethodIndex.NAMES[compareMethod], end).isTrue()) {
		size++;
		if (currentObject.equals(end)) {
		    break;
		}
		currentObject = currentObject.callMethod(context, "succ");
	    }
	}
        return getRuntime().newFixnum(size);
    }

    public override function op_equal(context:ThreadContext, other:IRubyObject, block:Block):IRubyObject{
        if (this == other) return getRuntime().getTrue();
        if (!(other instanceof RubyRange)) return getRuntime().getFalse();
        var otherRange: RubyRange =   other;
        var result: Boolean = 
            begin.op_equal(context, otherRange.begin).isTrue() &&
            end.op_equal(context, otherRange.end).isTrue() &&
            isExclusive == otherRange.isExclusive;
        return getRuntime().newBoolean(result);
    }
    
    public override function eql_p(other:IRubyObject):IRubyObject{
        if (this == other) return getRuntime().getTrue();
        if (!(other instanceof RubyRange)) return getRuntime().getFalse();
        var otherRange: RubyRange =  other;
        if (!begin.equals(otherRange.begin) || !end.equals(otherRange.end) || isExclusive != otherRange.isExclusive) return getRuntime().getFalse();
        return getRuntime().getTrue();
    }

    public function each0(context:ThreadContext, block:Block):IRubyObject{
        if (!begin.respondsTo("succ")) throw getRuntime().newTypeError("can't iterate from " + begin.getMetaClass().getName());

        if (begin instanceof RubyFixnum && end instanceof RubyFixnum) {
            var endLong: Number =  ( end).getLongValue();
            var i: Number =  ( begin).getLongValue();

            if (!isExclusive) {
                endLong += 1;
            }

            for (; i < endLong; i++) {
                block.yield(context, getRuntime().newFixnum(i));
            }
        } else if (begin instanceof RubyString) {
            ( begin).upto(context, end, isExclusive, block);
        } else if (getRuntime().getNumeric().isInstance(begin)) {
            if (!isExclusive) {
                end = end.callMethod(context, MethodIndex.OP_PLUS, "+", RubyFixnum.one(getRuntime()));
            }
            while (begin.callMethod(context, MethodIndex.OP_SPACESHIP, "<=>", end).convertToInteger().getLongValue() < 0) {
                block.yield(context, begin);
                begin = begin.callMethod(context, MethodIndex.OP_PLUS, "+", RubyFixnum.one(getRuntime()));
            }
        } else {
            var v: IRubyObject =  begin;

            if (isExclusive) {
                while (v.callMethod(context, MethodIndex.OP_SPACESHIP, "<=>", end).convertToInteger().getLongValue() < 0) {
                    if (v.equals(end)) {
                        break;
                    }
                    block.yield(context, v);
                    v = v.callMethod(context, "succ");
                }
            } else {
                while (v.callMethod(context, MethodIndex.OP_SPACESHIP, "<=>", end).convertToInteger().getLongValue() <= 0) {
                    block.yield(context, v);
                    if (v.equals(end)) {
                        break;
                    }
                    v = v.callMethod(context, "succ");
                }
            }
        }

        return this;
    }
    
    public  function step(context:ThreadContext, args:Array, block:Block):IRubyObject{
        var currentObject: IRubyObject =  begin;
        var compareMethod: int =  isExclusive ? MethodIndex.OP_LT : MethodIndex.OP_LE;
        // int stepSize = (int) (args.length == 0 ? 1 : args[0].convertToInteger().getLongValue());
        var stepSize: double =  1.0;
        if (args.length != 0) {
            stepSize = Double.parseDouble(args[0].toString());
        }
        if (stepSize == 0) {
            throw getRuntime().newArgumentError("step can't be 0");
        }
        else if (stepSize < 0) {
            throw getRuntime().newArgumentError("step can't be negative");
        }

        if (begin instanceof RubyFloat && end instanceof RubyFloat) {
            var stepNum: RubyFloat =  getRuntime().newFloat(stepSize);
            
            while (currentObject.callMethod(context, compareMethod, MethodIndex.NAMES.get(compareMethod), end).isTrue()) {
                block.yield(context, currentObject);
                currentObject = currentObject.callMethod(context, MethodIndex.OP_PLUS, "+", stepNum);
            }
        } else if (begin instanceof RubyNumeric && end instanceof RubyNumeric) {
            stepSize = Math.floor(stepSize);
            if (stepSize == 0) {
                throw getRuntime().newArgumentError("step can't be 0");
            }
            var stepNumN: RubyFixnum =  getRuntime().newFixnum(Double.valueOf(stepSize).longValue());
            
            while (currentObject.callMethod(context, compareMethod, MethodIndex.NAMES.get(compareMethod), end).isTrue()) {
                block.yield(context, currentObject);
                currentObject = currentObject.callMethod(context, MethodIndex.OP_PLUS, "+", stepNumN);
            }
        } else if(begin instanceof RubyString && end instanceof RubyString) {
          var afterEnd: RubyString =  isExclusive ?  end :  end.callMethod(context, "succ");
          var pastEnd: Boolean =  isExclusive && currentObject.callMethod(context, MethodIndex.EQUALEQUAL, "==", end).isTrue();  
          
          while(pastEnd == false) {
              block.yield(context, currentObject);
              for ( var int:i = 0; i < stepSize; i++) {
                  currentObject = currentObject.callMethod(context, "succ");
                  if(currentObject.callMethod(context, MethodIndex.EQUALEQUAL, "==", afterEnd).isTrue()) {
                    pastEnd = true;
                    break;
                  } 
              }
          }
        } else {
            if (!begin.respondsTo("succ")) throw getRuntime().newTypeError("can't iterate from " + begin.getMetaClass().getName());
            
            while (currentObject.callMethod(context, compareMethod, MethodIndex.NAMES.get(compareMethod), end).isTrue()) {
                block.yield(context, currentObject);
                
                for ( var int:i = 0; i < stepSize; i++) {
                    currentObject = currentObject.callMethod(context, "succ");
                }
            }
        }
        
        return this;
    }

    private  function r_lt(context:ThreadContext, a:IRubyObject, b:IRubyObject):Boolean{
        var r: IRubyObject =  a.callMethod(context, MethodIndex.OP_SPACESHIP, "<=>",b);

        if(r.isNil()) return false;

        return RubyComparable.cmpint(context, r, a, b) < 0;
        }

    private  function r_le(context:ThreadContext, a:IRubyObject, b:IRubyObject):Boolean{
        var r: IRubyObject =  a.callMethod(context, MethodIndex.OP_SPACESHIP, "<=>",b);

        if(r.isNil()) return false;

        return RubyComparable.cmpint(context, r, a, b) <= 0;
        }

    public  function include_p(context:ThreadContext, obj:IRubyObject, block:Block):RubyBoolean{
        var val: RubyBoolean =  getRuntime().getFalse();
        if(r_le(context, begin,obj)) {
            if(isExclusive) {
                if(r_lt(context, obj,end)) {
                    val = getRuntime().getTrue();
                }
            } else {
                if(r_le(context, obj,end)) {
                    val = getRuntime().getTrue();
                }
            }
        }
        return val;
    }
}
}